﻿using Microsoft.EntityFrameworkCore.Storage;
using Sgr.Domain.Uow.Database;
using Sgr.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Sgr.EntityFrameworkCore
{
    public class EfCoreTransactionApi : ITransactionApi, ISupportsRollback
    {
        private const int MinTimeout = 30000;

        private readonly IDbContextTransaction _dbContextTransaction;
        private readonly int? _timeout;
        private volatile bool _disposed;

        /// <summary>
        /// 获取数据库事务实例
        /// </summary>
        public IDbContextTransaction DbContextTransaction => _dbContextTransaction;

        /// <summary>
        /// 初始化事务API
        /// </summary>
        /// <param name="dbContextTransaction">数据库事务</param>
        /// <param name="timeout">超时时间（毫秒）</param>
        /// <exception cref="ArgumentNullException">当 dbContextTransaction 为 null 时抛出</exception>
        public EfCoreTransactionApi(
            IDbContextTransaction dbContextTransaction,
            int? timeout = null)
        {
            _dbContextTransaction = dbContextTransaction
                ?? throw new ArgumentNullException(nameof(dbContextTransaction));

            _timeout = ValidateTimeout(timeout);
        }

        #region ITransactionApi

        /// <summary>
        /// 异步提交事务
        /// </summary>
        /// <param name="cancellationToken">取消令牌</param>
        /// <remarks>
        /// 执行事务提交操作，将事务中的所有更改永久保存到数据库
        /// </remarks>
        /// <returns>表示异步操作的任务</returns>
        public virtual async Task CommitAsync(CancellationToken cancellationToken = default)
        {
            ThrowIfDisposed();

            // 创建超时令牌源
            using var timeoutCts = new CancellationTokenSource(
                TimeSpan.FromMilliseconds(_timeout ?? MinTimeout));

            // 组合取消令牌
            using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
                cancellationToken,
                timeoutCts.Token);

            await DbContextTransaction.CommitAsync(linkedCts.Token).ConfigureAwait(false);
        }

        #endregion ITransactionApi

        #region ISupportsRollback

        /// <summary>
        /// 异步回滚事务
        /// </summary>
        /// <param name="cancellationToken">取消令牌</param>
        /// <remarks>
        /// 执行事务回滚操作，撤销事务中的所有更改，
        /// 将数据库状态恢复到事务开始前的状态
        /// </remarks>
        /// <returns>表示异步操作的任务</returns>
        public virtual async Task RollbackAsync(CancellationToken cancellationToken = default)
        {
            ThrowIfDisposed();

            // 创建超时令牌源
            using var timeoutCts = new CancellationTokenSource(
                TimeSpan.FromMilliseconds(_timeout ?? MinTimeout));

            // 组合取消令牌
            using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
                cancellationToken,
                timeoutCts.Token);

            await DbContextTransaction.RollbackAsync(linkedCts.Token).ConfigureAwait(false);
        }

        #endregion ISupportsRollback

        /// <summary>
        /// 释放资源
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// 释放资源
        /// </summary>
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    //当 IDbContextTransaction 在未显式调用 CommitAsync 或 RollbackAsync 就被 Dispose 时，会自动执行回滚操作。
                    //它遵循了"失败时回滚"的原则，防止部分提交导致的数据不一致。
                    _dbContextTransaction.Dispose();
                }

                _disposed = true;
            }
        }

        /// <summary>
        /// 检查是否已释放
        /// </summary>
        protected void ThrowIfDisposed()
        {
            ObjectDisposedException.ThrowIf(_disposed, GetType().Name);
        }

        /// <summary>
        /// 验证超时设置
        /// </summary>
        private static int? ValidateTimeout(int? timeout)
        {
            if (!timeout.HasValue)
                return null;

            return Math.Max(timeout.Value, MinTimeout);
        }
    }
}